Catalyst / admin/Synapse-Sonar 14.8 GB / 57.8 GB 40.0 GB free
Help Sign in

admin / Synapse-Sonar

public

Attack Surface Simulation

Code Issues Pull requests Pipelines Packages Security Insights Wiki Settings
Synapse-Sonar / synapse-sonar / app / api / invite / [token] / route.ts 2865 B · main
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import bcrypt from "bcryptjs";
import { prisma } from "@/lib/prisma";

export const runtime = "nodejs";

/**
 * Invite acceptance — public (token-authenticated) flow.
 *
 *   GET  /api/invite/:token  → validate token, return display info for the page.
 *   POST /api/invite/:token  → set password + activate the account.
 *
 * The single-use invite token (created by POST /api/users) is the only
 * credential here; it is consumed (nulled) on successful acceptance.
 */

const ROLE_LABEL: Record<string, string> = {
  TENANT_ADMIN: "Tenant Admin",
  SECURITY_ANALYST: "Security Analyst",
  READ_ONLY_VIEWER: "Read-Only Viewer",
};

export async function GET(
  _req: NextRequest,
  { params }: { params: { token: string } }
) {
  const user = await prisma.user.findUnique({
    where: { inviteToken: params.token },
    include: { tenant: { select: { name: true, slug: true } } },
  });

  if (!user || user.status !== "INVITED") {
    return NextResponse.json(
      { error: "This invitation is invalid or has already been used." },
      { status: 404 }
    );
  }

  return NextResponse.json({
    username: user.username,
    name: user.name ?? "",
    roleLabel: ROLE_LABEL[user.role] ?? user.role,
    tenantName: user.tenant.name,
  });
}

const AcceptSchema = z.object({
  name: z.string().min(1).max(80).optional(),
  password: z.string().min(10, "Use at least 10 characters"),
});

export async function POST(
  req: NextRequest,
  { params }: { params: { token: string } }
) {
  let body;
  try {
    body = AcceptSchema.parse(await req.json());
  } catch (err) {
    if (err instanceof z.ZodError) {
      return NextResponse.json(
        { error: "Invalid input", details: err.issues },
        { status: 422 }
      );
    }
    return NextResponse.json({ error: "Invalid request" }, { status: 400 });
  }

  const user = await prisma.user.findUnique({
    where: { inviteToken: params.token },
  });

  if (!user || user.status !== "INVITED") {
    return NextResponse.json(
      { error: "This invitation is invalid or has already been used." },
      { status: 404 }
    );
  }

  // Consume the token, set credentials, activate. updateMany with the token
  // guard makes this idempotent-safe against a double submit.
  const result = await prisma.user.updateMany({
    where: { id: user.id, inviteToken: params.token, status: "INVITED" },
    data: {
      passwordHash: await bcrypt.hash(body.password, 12),
      status: "ACTIVE",
      inviteToken: null,
      ...(body.name ? { name: body.name } : {}),
    },
  });

  if (result.count === 0) {
    return NextResponse.json(
      { error: "This invitation is invalid or has already been used." },
      { status: 409 }
    );
  }

  return NextResponse.json({ ok: true, username: user.username });
}